async/await is syntactic sugar over Promises that interacts with the event loop by suspending function execution without blocking the thread, yielding control back to the event loop while waiting for a Promise to resolve, then continuing execution as a microtask.
async/await transforms asynchronous code to look synchronous while preserving JavaScript's non-blocking nature. When an async function encounters an await expression, it doesn't block the thread. Instead, it suspends the function's execution, returns control to the caller, and allows the event loop to continue processing other tasks. Once the awaited Promise resolves, the function's continuation is scheduled as a microtask. This interaction is fundamental to understanding how async/await fits into JavaScript's concurrency model and why it feels synchronous without actually blocking.
Async functions always return a Promise: Even if you return a primitive value, it's wrapped in a resolved Promise automatically .
Await unwraps Promises: The await keyword pauses execution until the awaited Promise settles, then unwraps the resolved value (or throws on rejection) .
State machine compilation: JavaScript engines (like V8) compile async functions into efficient state machines, similar to how generators work, with each await point becoming a state .
Suspension without blocking: When hitting await, the function's state (local variables, execution pointer) is saved, and control returns to the caller. The thread is free to handle other tasks .
Continuation as microtask: When the awaited Promise resolves, the function's continuation is scheduled as a microtask, giving it priority over macrotasks like timers or I/O .
The key insight is that each await creates a point where the function yields control. The code after the await doesn't execute immediately when the Promise resolves—it's placed in the microtask queue. This means other microtasks (like other promise callbacks or queueMicrotask calls) can run before the async function resumes, but macrotasks (like setTimeout) will wait until all microtasks are processed.
Microtask priority: Since async continuations are microtasks, they run before macrotasks. This ensures promises and async functions don't get starved by timers .
Sequential but non-blocking: Multiple awaits in sequence don't block—each suspension allows other code to run. The event loop processes other tasks between await points .
No stack traces across awaits: Because the function suspends, the call stack is not preserved across awaits. This is why async stack traces in errors are synthetically reconstructed .
Concurrency vs parallelism: Async/await enables concurrency (managing multiple tasks) but not parallelism. For CPU-bound work, consider Web Workers .
Error handling: Rejected Promises that are awaited throw exceptions that can be caught with try/catch, integrating seamlessly with synchronous error handling patterns .
A common misconception is that await blocks the thread. It does not. The thread continues executing other code, and the async function resumes when its continuation is pulled from the microtask queue. This is why you can have thousands of concurrent async operations—each is just a collection of state machines waiting for their turn on the event loop. The engine efficiently manages these using internal PromiseReaction jobs and microtask queues.
Understanding this interaction helps in debugging async code. For example, if you need to ensure something happens after an async operation but before any other microtasks, you can use await followed by immediate code—it will execute in the same microtask batch as the promise resolution. If you need to yield to the event loop to prevent blocking, you can await null or await Promise.resolve(), which schedules the continuation as a microtask but still allows other microtasks to run.